package pl.edu.mimuw.irs.core.model.simulation;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import pl.edu.mimuw.irs.core.model.analysis.History;
import pl.edu.mimuw.irs.core.model.analysis.Report;
import pl.edu.mimuw.irs.core.model.analysis.TPair;
import pl.edu.mimuw.irs.core.model.analysis.Time;
import pl.edu.mimuw.irs.core.model.analysis.atomic.ActivityPlace;
import pl.edu.mimuw.irs.core.model.analysis.resources.RequirementPlace;
import pl.edu.mimuw.irs.core.model.analysis.resources.ResourcePlace;
import pl.edu.mimuw.irs.core.model.analysis.workflow.AnalysisWorkflow;
import pl.edu.mimuw.irs.core.model.design.atomic.AtomicSubnet;
import pl.edu.mimuw.irs.core.model.design.net.IRSActivity;
import pl.edu.mimuw.irs.core.model.design.net.IRSWorkflow;
import pl.edu.mimuw.irs.core.model.design.resource.Requirement;
import pl.edu.mimuw.irs.core.model.petrinet.Place;
import pl.edu.mimuw.irs.core.model.petrinet.Transition;
import pl.edu.mimuw.irs.core.model.petrinet.workflow.Workflow;
import pl.edu.mimuw.irs.core.model.simulation.atomic.EventPlace;
import pl.edu.mimuw.irs.core.model.simulation.atomic.SimulationNodeFactory;
import pl.edu.mimuw.irs.core.model.simulation.exception.InternalSimulationException;
import pl.edu.mimuw.irs.core.model.simulation.exception.InternalSimulationException.SimulationCause;
import pl.edu.mimuw.irs.core.util.Converter;

public class SimulationWorkflow extends Workflow implements PropertyChangeListener {

	private static final long serialVersionUID = -8596955210932085012L;
	
	// *** Properties
	
	public static final String RUNS = "Runs";
	
	// *** Attributes

	private PropertyChangeSupport listeners;
	private AtomicSubnet atomicSubnet;
	/* 
	 * Stores all currently applicable runs (all that begin with the
	 * same steps and hirings as manually chosen steps - constraints).
	 */
	private List<History> runs;
	private List<History> tmpRuns;
	private Map<ActivityPlace, Map<RequirementPlace, Set<ResourcePlace>>> selection;
	
	private Map<IRSActivity, TPair> hiringMap;
	private Map<Requirement, RequirementPlace> requirementsMap;
	private History manualRun;
	private Time timestamp;
	
	public Map<ActivityPlace, Map<RequirementPlace, Set<ResourcePlace>>> getSelection() {
		if (selection == null) 
			selection = new LinkedHashMap<ActivityPlace, Map<RequirementPlace, Set<ResourcePlace>>>();
		return selection;
	}
	public PropertyChangeSupport getListeners() {
		if (listeners == null) listeners = new PropertyChangeSupport(this);
		return listeners;
	}
	public Map<IRSActivity, TPair> getHiringMap() {
		if (hiringMap == null) hiringMap = 
			new LinkedHashMap<IRSActivity, TPair>();
		return hiringMap;
	}
	public Map<Requirement, RequirementPlace> getRequirementsMap() {
		if (requirementsMap == null) requirementsMap =
			new LinkedHashMap<Requirement, RequirementPlace>();
		return requirementsMap;
	}
	public History getManualRun() {
		if (manualRun == null) manualRun = new History();
		return manualRun;
	}
	public Time getTimestamp() {
		if (timestamp == null) timestamp = new Time();
		return timestamp;
	}
	
	// *** Constructors
	
	public SimulationWorkflow(IRSWorkflow workflow,
			AnalysisWorkflow analysisWorkflow, 
			Report report) {
		atomicSubnet = workflow.getRootPlace().getAtomicWorkflow(
				new SimulationNodeFactory(getIdManager()));
		/* We will often modify this list so it must be a linked list */
		runs = new LinkedList<History>(report.getRuns());
		tmpRuns = new LinkedList<History>(report.getRuns());
		hiringMap = analysisWorkflow.getActivityHiringMap();
		requirementsMap = analysisWorkflow.getGraph().getRequirements();
		
		attachListeners(atomicSubnet);
		// FIXME !! merge with tmpRuns
		loadNet(atomicSubnet);
		getIn().setTokens(1);
	}
	
	// *** Methods
	
	private void attachListeners(AtomicSubnet atomicSubnet) {
		for (Place place : atomicSubnet.getPlaces()) {
			if (!(place instanceof EventPlace)) continue;
			EventPlace eventPlace = (EventPlace) place;
			eventPlace.addPropertyChangeListener(Place.TOKENS, this);
			eventPlace.addPropertyChangeListener(EventPlace.BUSY, this);
		}
	}
	
	public Map<RequirementPlace, Set<ResourcePlace>> getPossibilities(IRSActivity activity) {
		Map<RequirementPlace, Set<ResourcePlace>> possibilities = 
			new LinkedHashMap<RequirementPlace, Set<ResourcePlace>>();
		
		for (History history : tmpRuns) {
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = 
				history.getChronology().get(getTimestamp());
			TPair pair = getHiringMap().get(activity);
			// XXX : NPE handling
			Map<ResourcePlace, Set<RequirementPlace>> involved = 
				events.get(pair.getHire());
			if (involved == null) continue;
			
			for (Entry<ResourcePlace, Set<RequirementPlace>> entry : 
					involved.entrySet()) {
				for (RequirementPlace requirement : entry.getValue()) {
					Set<ResourcePlace> resources = possibilities.get(requirement);
					if (resources == null) {
						resources = new LinkedHashSet<ResourcePlace>();
						possibilities.put(requirement, resources);
					}
					resources.add(entry.getKey());
				}
			}
		}
		
		return possibilities;
	}
	
	public List<History> getTmpRuns(IRSActivity activity, RequirementPlace requirement, ResourcePlace resource) {
		List<History> applicableRuns = new LinkedList<History>();
		
		for (History history : tmpRuns) {
			/*
			 * Get current events.
			 */
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = 
				history.getChronology().get(getTimestamp());
			if (events == null) continue;
			
			/*
			 * Take under consideration only those which apply to this activity.
			 */
			TPair pair = getHiringMap().get(activity);
			// XXX : NPE handling
			Map<ResourcePlace, Set<RequirementPlace>> involved = 
				events.get(pair.getHire());
			if (involved == null) continue;
			
			/*
			 * Get only those which uses given resource for given requirement.
			 */
			Set<RequirementPlace> requirements = involved.get(resource);
			if (requirements == null) continue;
			if (!requirements.contains(requirement)) continue;
			
			/*
			 * This history applies to our activity + requirement + resource.
			 * So we can get it for analysis.
			 */
			applicableRuns.add(history);
		}
		
		return applicableRuns;
	}

	public List<History> getRuns(History run) {
		List<History> applicableRuns = new LinkedList<History>();
		
		for (History history : runs) {
			/*
			 * If run applies to our history we add it to the result.
			 */
			if (history.beginsWith(run)) applicableRuns.add(history);
		}
		
		return applicableRuns;
	}
	
	public void select(ActivityPlace activityPlace,
			RequirementPlace requirement, ResourcePlace resource) {
		List<History> oldRuns = tmpRuns;
		tmpRuns = getTmpRuns(activityPlace.getRequirement().getActivity(), 
				requirement, resource);

		Map<RequirementPlace, Set<ResourcePlace>> selectedRequirements = 
			getSelection().get(activityPlace);
		if (selectedRequirements == null) {
			selectedRequirements = 
				new LinkedHashMap<RequirementPlace, Set<ResourcePlace>>();
			getSelection().put(activityPlace, selectedRequirements);
		}
		Set<ResourcePlace> selectedResources = 
			selectedRequirements.get(requirement);
		if (selectedResources == null) {
			selectedResources = 
				new LinkedHashSet<ResourcePlace>();
			selectedRequirements.put(requirement, selectedResources);
		}
		selectedResources.add(resource);
		
		// TODO committedRequirements - Should handle double - but it does not.
		activityPlace.setTokens(selectedResources.size());
		
		getListeners().firePropertyChange(RUNS, oldRuns, tmpRuns);
	}
	
	public Map<RequirementPlace, Set<ResourcePlace>> getPossibilities(ActivityPlace place) {
		IRSActivity activity = place.getRequirement().getActivity();
		
		return getPossibilities(activity);
	}
	
	public Calculations getCalculations(ActivityPlace activityPlace,
			RequirementPlace requirement, ResourcePlace resource) {
		List<History> applicableRuns = 
				getTmpRuns(activityPlace.getRequirement().getActivity(), 
						requirement, resource);
		return new Calculations(applicableRuns);
	}
	
	/* 
	 * This method assumes that run begins with manualRun and does not check
	 * it.
	 */
	private Time getCurrentTimestamp(History run, History manualRun) {
		Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = 
			run.getChronology().get(getTimestamp());
		Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> manualEvents = 
			manualRun.getChronology().get(getTimestamp());
		if (manualEvents == null) return getTimestamp();
		
		/* If some events have not been started - the timestamp does not change. */
		if (events.size() != manualEvents.size()) return getTimestamp();
		for (Entry<Transition, Map<ResourcePlace, Set<RequirementPlace>>> event : 
				events.entrySet()) {
			Map<ResourcePlace, Set<RequirementPlace>> resources = 
					manualEvents.get(event.getKey());
			/* Resources map must not be empty - if is - we want NPE to be thrown. */
			for (Entry<ResourcePlace, Set<RequirementPlace>> entry : event.getValue().entrySet()) {
				Set<RequirementPlace> requirements = 
						resources.get(entry.getKey());
				if (requirements == null || 
						requirements.size() != entry.getValue().size())
					return getTimestamp();
				if (!requirements.containsAll(entry.getValue())) return getTimestamp();
			}
		}
		
		/* 
		 * If we are here it means, that all requirements of this timestamp 
		 * has been fulfilled. So now we have to find the soonest later one.
		 */
		Time lowestNext = null;
		for (Time time : run.getChronology().keySet()) {
			if (lowestNext == null) {
				if (time.compareTo(getTimestamp()) > 0) lowestNext = time;
			} else {
				if (lowestNext.compareTo(time) > 0) lowestNext = time;
			}
		}
		
		return lowestNext;
	}
	
	public void addPropertyChangeListener(PropertyChangeListener listener) {
		getListeners().addPropertyChangeListener(listener);
	}

	public void removePropertyChangeListener(PropertyChangeListener listener) {
		getListeners().removePropertyChangeListener(listener);
	}
	
	@Override
	public void propertyChange(PropertyChangeEvent arg0) {
		if (arg0.getPropertyName() != EventPlace.BUSY) return;
		if (!(arg0.getSource() instanceof ActivityPlace)) return;
		ActivityPlace activityPlace = (ActivityPlace) arg0.getSource();
		
		// TODO : assume all atomic activities have some cost
		/* Check if it was hire or fire */
		IRSActivity irsActivity = activityPlace.getRequirement().getActivity();
		//Boolean oldValue = (Boolean) arg0.getOldValue();
		Boolean newValue = (Boolean) arg0.getNewValue();
		Transition event;
		if (newValue == true) event = getHiringMap().get(irsActivity).getHire();
		else event = getHiringMap().get(irsActivity).getFire();
		
		for (History run : runs) {
			Time currentTime = getCurrentTimestamp(run, getManualRun());
			Map<Transition, Map<ResourcePlace, Set<RequirementPlace>>> events = 
				run.getChronology().get(currentTime);
			if (events == null) continue;
			if (!events.containsKey(event)) continue;
			
			/*
			 * TODO : Prove it is right - if any matches, then all matching
			 * will have the same timestamp.
			 */
			Map<RequirementPlace, Set<ResourcePlace>> selection = 
				getSelection().get(activityPlace);
			if (selection == null) selection = Collections.emptyMap();
			Map<ResourcePlace, Set<RequirementPlace>> swapped = 
				Converter.swap(selection);
			/*
			 * Mark activity execution.
			 */
			markExecution(currentTime, event, swapped);
			return;
		}
		
		/* XXX If no runs are possible something must have gone wrong */
		throw new InternalSimulationException(SimulationCause.NO_APPLICABLE_RUNS_FOUND, getManualRun());
	}
	
	private void markExecution(Time currentTime, Transition event,
			Map<ResourcePlace, Set<RequirementPlace>> swapped) {
		this.getManualRun().addEvent(currentTime, event, swapped);
		this.runs = getRuns(getManualRun());
		List<History> oldRuns = tmpRuns;
		this.tmpRuns = runs;
		this.selection.clear();
		this.timestamp = currentTime;
		getListeners().firePropertyChange(RUNS, oldRuns, tmpRuns);
	}
}
